# vPresent is an immersive presentation creation and display application.
# Copyright (C) 2007 by Infiscape Corporation
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

import os
import string
import traceback
from PyQt4 import QtXml

import opendocument as od
import gui.util

const = gui.util.const


# XML namespaces defined by the OpenDocument schema.
CORE_NAMESPACES = {
   'office'       : 'urn:oasis:names:tc:opendocument:xmlns:office:1.0',
   'meta'         : 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0',
   'config'       : 'urn:oasis:names:tc:opendocument:xmlns:config:1.0',
   'text'         : 'urn:oasis:names:tc:opendocument:xmlns:text:1.0',
   'table'        : 'urn:oasis:names:tc:opendocument:xmlns:table:1.0',
   'drawing'      : 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0',
   'draw'         : 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0',
   'presentation' : 'urn:oasis:names:tc:opendocument:xmlns:presentation:1.0',
   'dr3d'         : 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0',
   'anim'         : 'urn:oasis:names:tc:opendocument:xmlns:animation:1.0',
   'chart'        : 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0',
   'form'         : 'urn:oasis:names:tc:opendocument:xmlns:form:1.0',
   'script'       : 'urn:oasis:names:tc:opendocument:xmlns:script:1.0',
   'style'        : 'urn:oasis:names:tc:opendocument:xmlns:style:1.0',
   'number'       : 'urn:oasis:names:tc:opendocument:xmlns:data style:1.0',
   'manifest'     : 'urn:oasis:names:tc:opendocument:xmlns:manifest:1.0',
}

# XML namespaces defined by the OpenDocument schema that include elements
# and attributes compatible to elements and attributes of other standards.
COMPAT_NAMESPACES = {
   'fo'   : 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0',
   'svg'  : 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0',
   'smil' : 'urn:oasis:names:tc:opendocument:xmlns:smil-compatible:1.0',
}

# Standard XML namespaces used by the OpenDocument schema.
STD_NAMESPACES = {
   'dc'     : 'http://purl.org/dc/elements/1.1/',
   'xlink'  : 'http://www.w3.org/1999/xlink',
   'math'   : 'http://www.w3.org/1998/Math/MathML',
   'xforms' : 'http://www.w3.org/2002/xforms',
   'xsd'    : 'http://www.w3.org/2001/XMLSchema',
}

CUSTOM_NAMESPACES = {
   'ooo'      : 'http://openoffice.org/2004/office',
   'vpresent' : 'http://realityforge.vrsource.org/vpresent/1.0'
}

ALL_NAMESPACES = {}
ALL_NAMESPACES.update(CORE_NAMESPACES)
ALL_NAMESPACES.update(COMPAT_NAMESPACES)
ALL_NAMESPACES.update(STD_NAMESPACES)
ALL_NAMESPACES.update(CUSTOM_NAMESPACES)

def createXMLDocument():
   doc = QtXml.QDomDocument()
   doc.appendChild(
      doc.createProcessingInstruction(
         'xml', 'version="1.0" encoding="UTF-8"'
      )
   )

   return doc

def createContentRoot(document):
   office_ns = CORE_NAMESPACES['office']
   root = document.createElementNS(office_ns, 'office:document-content')

   for ns, uri in CORE_NAMESPACES.items():
      if ns != 'office' and ns != 'manifest':
         root.setAttribute('xmlns:%s' % ns, uri)

   for ns, uri in COMPAT_NAMESPACES.items():
      root.setAttribute('xmlns:%s' % ns, uri)

   for ns, uri in STD_NAMESPACES.items():
      root.setAttribute('xmlns:%s' % ns, uri)

   for ns, uri in CUSTOM_NAMESPACES.items():
      root.setAttribute('xmlns:%s' % ns, uri)

   root.setAttributeNS(office_ns, 'office:version',
                       str(od.getOpenDocVersion()))

   #root.appendChild(document.createElementNS(office_ns, 'office:scripts'))
   #root.appendChild(document.createElementNS(office_ns,
   #                                          'office:automatic-styles'))

   #body = document.createElementNS(office_ns, 'office:body')
   #root.appendChild(body)

   return root

def createMetaRoot(document, contentType):
   office_ns = CORE_NAMESPACES['office']
   root = document.createElementNS(office_ns, 'office:document-meta')
   root.setAttribute('xmlns:xlink', STD_NAMESPACES['xlink'])
   root.setAttribute('xmlns:dc', STD_NAMESPACES['dc'])
   root.setAttribute('xmlns:meta', CORE_NAMESPACES['meta'])
   root.setAttribute('xmlns:%s' % contentType, CORE_NAMESPACES[contentType])
   root.setAttribute('xmlns:vpresent', CUSTOM_NAMESPACES['vpresent'])
   root.setAttributeNS(office_ns, 'office:version',
                       str(od.getOpenDocVersion()))

   meta = document.createElementNS(office_ns, 'office:meta')
   root.appendChild(meta)

   return root

def createSettingsRoot(document, contentType):
   office_ns = CORE_NAMESPACES['office']
   root = document.createElementNS(office_ns, 'office:document-settings')
   root.setAttribute('xmlns:xlink', STD_NAMESPACES['xlink'])
   root.setAttribute('xmlns:dc', STD_NAMESPACES['dc'])
   root.setAttribute('xmlns:config', CORE_NAMESPACES['config'])
   root.setAttribute('xmlns:%s' % contentType, CORE_NAMESPACES[contentType])
   root.setAttribute('xmlns:vpresent', CUSTOM_NAMESPACES['vpresent'])
   root.setAttributeNS(office_ns, 'office:version',
                       str(od.getOpenDocVersion()))

   settings = document.createElementNS(office_ns, 'office:settings')
   root.appendChild(settings)

   config_ns = CORE_NAMESPACES['config']
   cfg_item_set = document.createElementNS(config_ns,
                                           'config:config-item-set')
   settings.appendChild(cfg_item_set)
   cfg_item_set.setAttributeNS(config_ns, 'config:name',
                               'vpresent:view-settings')

   cfg_item_set = document.createElementNS(config_ns,
                                           'config:config-item-set')
   settings.appendChild(cfg_item_set)
   cfg_item_set.setAttributeNS(config_ns, 'config:name',
                               'vpresent:configuration-settings')

   return root

def createStylesRoot(document):
   office_ns = CORE_NAMESPACES['office']
   root = document.createElementNS(office_ns, 'office:document-styles')

   for ns, uri in CORE_NAMESPACES.items():
      if ns != 'office' and ns != 'manifest':
         root.setAttribute('xmlns:%s' % ns, uri)

   for ns, uri in COMPAT_NAMESPACES.items():
      root.setAttribute('xmlns:%s' % ns, uri)

   for ns, uri in STD_NAMESPACES.items():
      root.setAttribute('xmlns:%s' % ns, uri)

   for ns, uri in CUSTOM_NAMESPACES.items():
      root.setAttribute('xmlns:%s' % ns, uri)

   root.setAttributeNS(office_ns, 'office:version',
                       str(od.getOpenDocVersion()))

   return root

def createManifestRoot(document, contentMimeType):
   manifest_ns = CORE_NAMESPACES['manifest']
   root = document.createElementNS(manifest_ns, 'manifest:manifest')

   entry = document.createElementNS(manifest_ns, 'manifest:file-entry')
   root.appendChild(entry)
   entry.setAttributeNS(manifest_ns, 'manifest:media-type', contentMimeType)
   entry.setAttributeNS(manifest_ns, 'manifest:full-path', '/')

   return root

class Reader:
   '''
   Basic document reader for OpenDocument XML data.  This covers metadata,
   styles, settings, and the content.  All must be presented as the root nodes
   for the respective data types.  In this way, this class can be used for
   reading those OpenDocument documents that are single XML files and those
   that are split up among multiple files.
   '''

   def __init__(self, builder):
      self.builder = builder

   def construct(self, docRoot, metaRoot, stylesRoot, settingsRoot):
      ver = docRoot.attributeNS(CORE_NAMESPACES['office'], 'version')

      if ver > str(od.getOpenDocVersion()):
         raise Exception("OpenDocument %s not supported" % ver)

      self.builder.buildContent()

      if settingsRoot is not None:
         self.__buildSettings(settingsRoot)

      if metaRoot is not None:
         self.__processTree(metaRoot)

      if stylesRoot is not None:
         self.__processTree(stylesRoot)

      self.__processTree(docRoot)

      # Inform the builder that we are done reading.
      self.builder.finish()

   def __processTree(self, node):
      ''' Process a top level document and close the container. '''
      self.__buildElement(node.toElement())
      self.builder.closeContainer()

   def __buildElement(self, elt):
      # Build a document node and find out if it accepts children.
      accepts_children = self.builder.buildDocumentNode(str(elt.nodeName()), self.__attrsToDict(elt))

      # If the builder says that the element has children, process them.
      if accepts_children:
         children = elt.childNodes()
         for i in xrange(children.count()):
            self.__elementHandler(children.item(i))

         # Close the container when we are done with the children.
         self.builder.closeContainer()

   def __elementHandler(self, node):
      """ If given node is a TEXT_NODE then inform the builder.
          Otherwise process the node as we normally do.
      """
      if node.isText():
         self.__buildText(node)
      elif node.isElement():
         self.__buildElement(node.toElement())
      else:
         print "WARNING: Unexpected content type %d as child of <%s>!" % \
                  (node.nodeType(), node.parentNode().nodeName())

   def __buildSettings(self, settingsRoot):
      settings_node = settingsRoot.firstChildElement('settings')

      if settings_node is not None and not settings_node.isNull():
         cfg_item_sets = \
            settings_node.elementsByTagNameNS(CORE_NAMESPACES['config'],
                                              'config-item-set')
         for i in xrange(cfg_item_sets.count()):
            cfg_item_set = cfg_item_sets.item(i).toElement()
            cfg_name = str(cfg_item_set.attributeNS(CORE_NAMESPACES['config'],
                                                    'name'))

            self.__buildConfigItemSet(cfg_item_set)

   def __buildConfigItemSet(self, cfgItemSet):
      """ Build a set of configuration objects. """
      self.builder.build_config_ItemSet(self.__attrsToDict(cfgItemSet))

      children = cfgItemSet.childNodes()
      for i in xrange(children.count()):
         child = children.item(i)
         if child.isElement():
            self.__handleConfigElement(child.toElement())

      self.builder.closeContainer()

   def __handleConfigElement(self, elt):
      """ Build a configuration object from it's config element. """
      tag_name = elt.tagName()

      if tag_name == 'config-item':
         children = elt.childNodes()
         assert 1 == children.size()
         self.builder.buildDocumentNode(str(elt.nodeName()), self.__attrsToDict(elt))
         self.__buildText(children.at(0))
         self.builder.closeContainer()
      elif tag_name == 'config-item-set':
         self.__buildConfigitemSet(elt)
      elif tag_name == 'config-item-map-named':
         self.__buildConfigItemMapNamed(elt)
      elif tag_name == 'config-item-map-indexed':
         self.__buildConfigItemMapIndexed(elt)

   def __buildConfigItemMapNamed(self, cfgItemMap):
      self.__buildConfigItemMap(cfgItemMap,
                                self.builder.build_config_NamedConfigItemMap)

   def __buildConfigItemMapIndexed(self, cfgItemMap):
      self.__buildConfigItemMap(cfgItemMap,
                                self.builder.build_config_IndexedConfigItemMap)

   def __buildConfigItemMap(self, cfgItemMap, begin):
      # A config item map (indexed or named) contains one or more config item
      # map entries. All elements processed between here and the invocation of
      # end (see below) will be part of the current config item map.
      begin(self.__attrsToDict(cfgItemMap))

      items = cfgItemMap.elementsByTagNameNS(CORE_NAMESPACES['config'],
                                             'config-item-map-entry')

      # A config:config-item-map-entry element holds one or more settings
      # elements.
      for i in xrange(items.count()):
         entry = items.item(i)

         # Start the current config item map entry.
         self.builder.build_config_ItemMapEntry(self.__attrsToDict(entry))

         # Now, we recurse because the structure is recursive. Each child may
         # be a different settings element.
         children = entry.childNodes()
         for j in xrange(children.count()):
            self.__handleConfigElement(children.item(i).toElement())

         # The current config item map entry is done.
         self.builder.closeContainer()

      # The current config item map is done.
      self.builder.closeContainer()

   def __buildText(self, textNode):
      self.builder.buildText(str(textNode.nodeValue()))

   def __attrsToDict(self, node):
      return self.__createDictionary(node.attributes())

   def __createDictionary(self, namedNodeMap):
      '''
      Converts the given DOM named node map into a Python dictionary.  Besides
      converting QString objects to Python str objects, the only processing
      performed on the contents of the node map prior to insertion into the
      dictionary is to strip leading and trailing whitespace from the node
      value.

      @param namedNodeMap A QDomNamedNodeMap object.

      @return A Python dictionary mapping the node names to the node values.
      '''
      dict = {}

      for i in xrange(namedNodeMap.count()):
         item = namedNodeMap.item(i)
         dict[unicode(item.nodeName())] = unicode(item.nodeValue()).strip()

      return dict

class Writer:
   def write(self, document, contentDoc, contentRoot, metaDoc, metaRoot,
             settingsDoc, settingsRoot, stylesDoc, stylesRoot):
      self.contentDoc   = contentDoc
      self.contentRoot  = contentRoot
      self.metaDoc      = metaDoc
      self.metaRoot     = metaRoot
      self.settingsDoc  = settingsDoc
      self.settingsRoot = settingsRoot
      self.stylesDoc    = stylesDoc
      self.stylesRoot   = stylesRoot

      office_ns = CORE_NAMESPACES['office']

      self.settingsNode = self.settingsRoot.firstChildElement('settings')

      if self.settingsNode is None or self.settingsNode.isNull():
         self.settingsNode = \
            self.settingsDoc.createElementNS(office_ns, 'office:settings')
         self.settingsRoot.appendChild(self.settingsNode)

      self.metaNode = self.metaRoot.firstChildElement('meta')

      if self.metaNode is None or self.metaNode.isNull():
         self.metaNode = self.metaDoc.createElementNS(office_ns,
                                                      'office:meta')
         self.metaRoot.appendChild(self.metaNode)

      self.contentNode = self.contentRoot
      if self.contentNode is None or self.contentNode.isNull():
         self.contentNode = createContentRoot(self.contentDoc)
         self.contentRoot.appendChild(self.contentNode)

      self.stylesNode = self.stylesRoot
      if self.stylesNode is None or self.stylesNode.isNull():
         self.stylesNode = createStylesRoot(self.contentDoc)
         self.contentRoot.appendChild(self.stylesNode)

      self.curDoc           = None
      self.curContainerNode = None
      self.containerStack   = []

      # Traverse over each document.
      metadata = document.getMetadata()
      self.curDoc = metaDoc
      self.__setNewContainer(metaRoot)
      gui.util.traverse(metadata._children, self.enterNode, self.exitNode)
      self.closeContainer()

      settings = document.getSettings()
      self.curDoc = settingsDoc
      self.__setNewContainer(settingsRoot)
      gui.util.traverse(settings._children, self.enterNode, self.exitNode)
      self.closeContainer()

      content = document.getContent()
      self.curDoc = contentDoc
      self.__setNewContainer(contentRoot)
      gui.util.traverse(content._children, self.enterNode, self.exitNode)
      self.closeContainer()

      styles = document.getStyles()
      self.curDoc = stylesDoc
      self.__setNewContainer(stylesRoot)
      gui.util.traverse(styles._children, self.enterNode, self.exitNode)
      self.closeContainer()

      for n in [self.metaRoot, self.settingsRoot]:
         n.setAttribute('xmlns:smil', COMPAT_NAMESPACES['smil'])
         n.setAttribute('xmlns:anim', CORE_NAMESPACES['anim'])

   def enterNode(self, node):
      if isinstance(node, od.PlainText):
         xml_node = self.create_PlainText(node)
      else:
         method_name = 'create_%s' % od.tagToType(node.element.name)
         if hasattr(self, method_name):
            xml_node = getattr(self, method_name)(node)
         else:
            #print "WARNING: Unhandled node type", node.__class__
            #print "         Override %s to eliminate this warning." % method_name
            xml_node = self.createDefaultNode(node)

      if xml_node is not None and node.acceptsChildren():
         self.__setNewContainer(xml_node)

      return gui.util.CONTINUE

   def exitNode(self, node, result):
      if node.acceptsChildren():
         self.closeContainer()
      return result

   def closeContainer(self):
      '''
      Closes the current container and makes the top of the container stack
      the new current container.  If the container stack is empty, then there
      is no active container object.

      @post self.curContainerNode holds one less entry. self.curContainerNode
            refers to the top of the container stack prior to the invocation
            of this method or None if the container stack was empty.
      '''
      #print "---"
      #print "Closing container", self.curContainerNode.nodeName()
      stack_len = len(self.containerStack)
      if stack_len > 0:
         self.curContainerNode = self.containerStack.pop()
         #print "Container is now", self.curContainerNode.nodeName()
      else:
         self.curContainerNode = None
         #print "No container"
      #print "---"

   def create_PlainText(self, node):
      value = node._content
      if value is not None:
         tn = self.curDoc.createTextNode(str(value))
         self.curContainerNode.appendChild(tn)
         return tn

   def createDefaultNode(self, node):
      tagName = node.element.name

      if tagName.find(':') == -1:
         elt = self.curDoc.createElement(tagName)
      else:
         ns_name = tagName.split(':')[0]
         elt = self.curDoc.createElementNS(ALL_NAMESPACES[ns_name], tagName)

      for k, v in node.getRawAttributes().items():
         v = unicode(v)

         if k.find(':') == -1:
            elt.setAttribute(k, v)
         else:
            ns_name = k.split(':')[0]
            elt.setAttributeNS(ALL_NAMESPACES[ns_name], k, v)

      #print "Appending child of type", elt.nodeName()
      self.curContainerNode.appendChild(elt)

      return elt

   def __setNewContainer(self, container):
      if self.curContainerNode is not None:
         self.containerStack.append(self.curContainerNode)

      self.curContainerNode = container

      #print "+++"
      #print "Changed container to", self.curContainerNode.nodeName()
      #print "\tContainer stack:"
      #vals = range(len(self.containerStack))
      #vals.reverse()
      #for i in vals:
      #   print "\t\t%d: %s" % (i, str(self.containerStack[i].nodeName()))
      #print "+++"

   def _cleanChildren(self, doc, root):
      '''
      Removes redundant XML namespace declarations from the children of root.
      This leaves the tree under root in a state that is really only useful
      for being written to a string or to a stream.
      '''
      old_children = []
      new_children = []

      children = root.childNodes()
      for i in xrange(children.count()):
         child = children.item(i)
         new_child = self.__stripExtraNS(doc, child)
         old_children.append(child)
         new_children.append(new_child)

      for c in old_children:
         root.removeChild(c)

      for c in new_children:
         root.appendChild(c)

   def __stripExtraNS(self, doc, node):
      '''
      Removes redundant XML namespace declarations from node and the children
      of node. This is done by creating a new element node with new
      attributes, neither of which use namespace information but still retain
      the correct qualified name. This is done because QtXml puts namespace
      declarations for every namespace referenced by a node and its elements
      even if the namespaces have already been declared.
      '''
      if node.isElement():
         clean_node = doc.createElement(node.nodeName())

         attrs = node.attributes()
         for i in xrange(attrs.count()):
            attr = attrs.item(i)
            clean_node.setAttribute(attr.nodeName(), attr.nodeValue())

         children = node.childNodes()
         for i in xrange(children.count()):
            clean_node.appendChild(self.__stripExtraNS(doc, children.item(i)))

         return clean_node
      else:
         return node
